<!DOCTYPE html>
<html lang="en">

    <head>
        <title>Snowboy Personal Wake Word</title>

        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="Record your own snowboy personal wake word">
        <meta name="author" content="Michael Hansen (synesthesiam)">

        <link rel="icon" href="img/favico.png">
        <link href="css/bootstrap.min.css" rel="stylesheet">
        <link href="css/fontawesome-all.min.css" rel="stylesheet">

        <script src="js/wavesurfer.js"></script>
        <script src="js/wavesurfer.microphone.min.js"></script>
    </head>
    <body>
        <form id="form" method="POST" onsubmit="submitAudio()">
            <div class="container-fluid mt-3 mb-3">
                <div class="row-fluid">
                    <h1>Snowboy Personal Wake Word Recorder</h1>
                </div>
                <div class="row-fluid">
                    <p>
                        Record 3 wake word examples and submit them to generate a <tt>.pmdl</tt> file.
                    </p>
                    <ol>
                        <li>Enable microphone (required for Chrome)</li>
                        <li>Click Record and wait until ready</li>
                        <li>Speak your wake word and <strong>wait for end</strong></li>
                        <li>Repeat until you have 3 examples</li>
                        <li>Enter a model name, submit audio, and click "Save Model" button</li>
                    </ol>
                </div>
                <div class="row-fluid">
                    <div id="alert" class="alert alert-info">Waiting for microphone to be enabled</div>
                </div>
                <div class="row-fluid">
                    <div id="waveform"></div>
                </div>
                <div class="control-group mt-3">
                    <div class="controls row-fluid">
                        <button type="button" id="record" class="btn btn-lg btn-danger" data-action="record" hidden>
                            <i id="recordIcon" class="fas fa-microphone mr-1"></i>
                            <span id="recordText">Record</span>
                        </button>
                        <button type="button" id="enable" class="btn btn-lg btn-primary" data-action="enable">
                            <i class="fas fa-power-off mr-1"></i>
                            Enable Microphone
                        </button>
                    </div>
                </div>
                <div class="form-group mt-3">
                    <div class="form-row">
                        <div class="col-auto">
                            <label for="modelName" class="form-label">Model Name:</label>
                        </div>
                        <div class="col">
                            <input type="text" class="form-control" name="modelName" id="modelName" placeholder="Model name">
                        </div>
                    </div>
                </div>

                <div class="control-group mt-5">
                    <div class="controls row">
                        <div class="col">
                            <h3>Examples (3 required)</h3>
                        </div>
                    </div>
                    <div class="controls row">
                        <div class="col">
                            <ol id="examples">
                            </ol>
                        </div>
                    </div>
                </div>

                <div class="control-group mt-5">
                    <div class="controls row">
                        <div class="col-auto">
                            <button id="submit" type="submit" class="btn btn-lg btn-success" data-action="submit">
                                <i class="fas fa-upload mr-1"></i>
                                Submit
                            </button>
                            <i id="submittedIcon" class="fas fa-thumbs-up ml-2" title="Submitted" hidden></i>
                        </div>
                        <div class="col">
                            <a id="download" href="#" class="btn btn-lg btn-primary" style="display: none;">
                                <i class="fas fa-download mr-2"></i>
                                Save Model
                            </a>
                        </div>
                    </div>
                </div>
            </div>
        </form>

        <script>
         var wavesurfer = {};
         var recording = false;
         var recorder = null;
         var chunks = [];
         var blobs = {};
         var audioFormat = null;

         // Delay before recording starts
         const numWaitSeconds = 2;
         var waitSecondsLeft = 0;

         // Seconds to record
         const numRecordSeconds = 3;
         var recordSecondsLeft = 0;

         function q(id) {
             return document.querySelector(id);
         }

         function setAlert(text, className) {
             q('#alert').classList.remove('alert-info');
             q('#alert').classList.remove('alert-danger');
             q('#alert').classList.remove('alert-success');
             q('#alert').classList.remove('alert-warning');

             q('#alert').classList.add(className);
             q('#alert').textContent = text;
         }

         function startRecording() {
             // Update record button until timeout
             if (waitSecondsLeft > 1) {
                 waitSecondsLeft -= 1;
                 q('#recordText').textContent = 'Wait...' + waitSecondsLeft;
                 setTimeout(startRecording, 1000);
                 return;
             }

             // Start recording
             chunks = [];
             recorder.start(500);
             recorder.ondataavailable = function(e) {
                 chunks.push(e.data);
             };

             recordSecondsLeft = numRecordSeconds;
             q('#recordIcon').classList.remove('fa-hand-paper');
             q('#recordIcon').classList.add('fa-thumbs-up');
             q('#recordText').textContent = 'Speak...' + recordSecondsLeft;
             q('#record').classList.remove("btn-warning");
             q('#record').classList.add("btn-success");

             // Check recording timeout
             setTimeout(stopRecording, 1000);
             setAlert('Recording for ' + recordSecondsLeft + ' seconds', 'alert-warning');
         }

         function stopRecording() {
             // Check for recording timeout
             if (recordSecondsLeft > 1) {
                 recordSecondsLeft -= 1;
                 q('#recordText').textContent = 'Speak...' + recordSecondsLeft;
                 setTimeout(stopRecording, 1000);
                 return;
             }

             // Done recording
             recorder.stop();

             // Convert to blob and keep with timestamp
             const blob = new Blob(chunks, { 'type' : 'audio/webm' });
             const timestamp = Date.now();
             const blobId = 'blob_' + timestamp;
             blobs[blobId] = blob;

             // Create new listen item and add audio
             const li = document.createElement('li');
             li.setAttribute('id', 'li-' + timestamp);
             li.classList.add('mb-3');
             q('#examples').appendChild(li);

             const clip = document.createElement("audio");
             clip.setAttribute('id', 'clip-' + timestamp);
             clip.setAttribute('controls', '');
             clip.setAttribute('src', window.URL.createObjectURL(blob));
             clip.classList.add('align-top');
             li.appendChild(clip);

             const deleteButton = document.createElement("button");
             deleteButton.setAttribute('type', 'button');
             deleteButton.setAttribute('title', 'Delete example');
             deleteButton.classList.add('btn');
             deleteButton.classList.add('btn-danger');
             deleteButton.classList.add('align-top');
             deleteButton.classList.add('ml-3');
             li.appendChild(deleteButton);

             deleteButton.addEventListener('click', function() {
                 delete blobs[blobId];
                 li.remove();
             });

             const deleteIcon = document.createElement("i");
             deleteIcon.classList.add('fas');
             deleteIcon.classList.add('fa-inverse');
             deleteIcon.classList.add('fa-trash');
             deleteButton.appendChild(deleteIcon);

             // Reset UI for next recording
             q('#recordIcon').classList.remove('fa-thumbs-up');
             q('#recordIcon').classList.add('fa-microphone');
             q('#recordText').textContent = 'Record';
             q('#record').classList.remove("btn-success");
             q('#record').classList.add("btn-danger");

             q('#record').disabled = false;
             q('#submit').disabled = false;

             setAlert('Done recording', 'alert-success');
         }

         async function submitAudio() {
             // POST to /generate
             event.preventDefault();

             // Ensure model name is set
             const modelName = q('#modelName').value || '';
             if (modelName.length == 0) {
                 alert('Model name is required');
                 return;
             }

             // Check for at least 3 examples
             if (Object.keys(blobs).length < 3) {
                 alert('Need at least 3 examples');
                 return;
             }

             const formData = new FormData(q('#form'));
             const formPage = 'generate';

             setAlert('Submitting data...', 'alert-warning');

             // Attach each audio blob as a file
             Object.keys(blobs).forEach(function(k) {
                 formData.set(k, blobs[k], k);
             });

             formData.set('format', audioFormat);

             let response = await fetch(formPage, {
                 method: 'POST',
                 body: formData
             })
                 .then(response => {
                     if (!response.ok) {
                         throw Error(response.statusText);
                     }

		     return response.blob();
		 })
                 .then(blob => {
                     // Show download link
                     const modelName = q('#modelName').value;

                     var url = window.URL.createObjectURL(blob);
                     q('#download').setAttribute('href', url);
                     q('#download').setAttribute('download', modelName + '.pmdl');
                     q('#download').style.display = '';

                     setAlert('Microphone ready!', 'alert-success');
                 })
                 .catch((error) => {
                     setAlert('Failed to submit: ' + error, 'alert-danger');
                 });

         }

         document.addEventListener('DOMContentLoaded', function() {
             q('#record').disabled = true;
             q('#submit').disabled = true;
             q('#download').disabled = true;

             // webm or wav
             const preferredFormat = 'audio/ogg; codecs=opus';
             const audio = document.createElement('audio');
             audioFormat = audio.canPlayType(preferredFormat)
                          ? preferredFormat
                          : 'audio/wav';

             wavesurfer = WaveSurfer.create({
                 container: q('#waveform'),
                 interact: false,
                 waveColor: '#000',
                 cursorWidth: 0,
                 plugins: [
                     WaveSurfer.microphone.create()
                 ]
             });

             wavesurfer.microphone.on('deviceReady', function(stream) {
                 // This is why I hate web dev
                 if (MediaRecorder === undefined) {
                     recorder = new window.MediaRecorder(stream);
                 } else {
                     recorder = new MediaRecorder(stream);
                 }

                 q('#record').disabled = false;
                 setAlert('Microphone ready!', 'alert-success');
             });

             wavesurfer.microphone.on('deviceError', function(code) {
                 recorder = null;
                 q('#record').disabled = true;
                 setAlert('Device error: ' + code, 'alert-danger');
             });

             // Enable microphone (required to work in Chrome)
             q('[data-action="enable"]')
                 .addEventListener('click', function() {
                     wavesurfer.microphone.start();
                     q('#record').hidden = false;
                     q('#enable').hidden = true;
                 });

             // Wait for a few seconds, then record for a few seconds.
             // We do it this way so button clicks don't end up in the audio and
             // we don't have to detect silence.
             // Silence will be automatically by the web server.
             q('[data-action="record"]')
                 .addEventListener('click', function() {
                     q('#record').disabled = true;
                     q('#record').classList.remove("btn-danger");
                     q('#record').classList.add("btn-warning");

                     // Start waiting
                     waitSecondsLeft = numWaitSeconds;
                     q('#recordIcon').classList.remove('fa-microphone');
                     q('#recordIcon').classList.add('fa-hand-paper');
                     q('#recordText').textContent = 'Wait...' + waitSecondsLeft;

                     setTimeout(startRecording, 1000);
                 });

         });
        </script>
    </body>
</html>
